#ifndef __SELECT_SERVER__
#define __SELECT_SERVER__
#include "Socket.hpp"
#include <sys/select.h>

static const uint16_t DEFAULT_PORT = 9000;
static const int DEFAULT_FD = -1;       // fdArray默认FD是-1
static const int ARRAY_SIZE = 1024;     // fdArray的长度是1024
extern Log log;

class SelectServer
{
public:
    SelectServer(const uint16_t& port = DEFAULT_PORT) : _port(port), _fdArray(new int[ARRAY_SIZE])
    {
        // 初始化fdArray，全部初始化为-1
        for(int i = 0; i < ARRAY_SIZE; ++i) {
            _fdArray[i] = DEFAULT_FD;
        }
    }  

    ~SelectServer()
    {
        delete _fdArray;
    }

    void initServer()
    {
        _listenSocket.Socket();
        // 允许套接字绑定到一个处于 TIME_WAIT 状态的端口
        int reuse = 1;
        setsockopt(_listenSocket.GetFd(), SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &reuse, sizeof(reuse));
        _listenSocket.Bind(_port);
        _listenSocket.Listen();
    }

    void startServer()
    {
        /* accept会阻塞等待客户端的连接，一次只能等一个，
        但我们这是一个实现多路转接的服务器，需要一次等待多个客户端，所以要先select
        每次有一个新连接到来，等价于读事件就绪。*/ 

        int listenSocket = _listenSocket.GetFd();
        _fdArray[0] = listenSocket;     // 第一个元素是监听套接字的fd
        for(; ;) {
            // 初始化读位图
            fd_set readfds;
            FD_ZERO(&readfds);
            int maxFd = listenSocket;   // 最大的套接字，用于设置select的第一个参数

            // 该循环每次都会根据_fdArray更新readfds
            for(int i = 0; i < ARRAY_SIZE; ++i) {
                if(_fdArray[i] == -1)    continue;
                // 不是-1，证明该文件描述符需要被监视，将其加入到数组
                FD_SET(_fdArray[i], &readfds);
                if(_fdArray[i] > maxFd) {
                    maxFd = _fdArray[i];
                    log(INFO, "Max fd update: %d\n", maxFd);
                }
            }

            // 进行select
            timeval tv = {2, 0};        // 设置2s超时，由于是输入输出参数，所以可能需要周期重复的设置
            // int n = select(maxFd + 1, &readfds, nullptr, nullptr, &tv);
            int n = select(maxFd + 1, &readfds, nullptr, nullptr, nullptr);
            // 判断n，进行不同的操作
            switch (n)
            {
            case 0:
                log(INFO, "Time out!: %d, %d\n", tv.tv_sec, tv.tv_usec);
                sleep(1);
                break;
            case -1:
                log(ERROR, "Select error!\n");
                break;
            default:
                // 有事件就绪了。如果事件就绪，上层不处理，select就会一直通知用户，让其处理
                // 一个事实是：如果select告诉用户数据已经就绪了，接下来，用户读取fd时不会被阻塞。      
                log(INFO, "Get a new link!\n");
                handlerEvent(&readfds);    
                break;
            }
        }
    }

private:
    bool handlerEvent(fd_set* set)
    {   
        // 这个循环处理_fdArray中所有已经就绪的事件，包括监听套接字创建连接和读事件
        for(int i = 0; i < ARRAY_SIZE; ++i) {
            if(_fdArray[i] == DEFAULT_FD)
                continue;
            int fd = _fdArray[i];

            if(fd == _listenSocket.GetFd() && FD_ISSET(fd, set)) {
                // 是监听套接字并且有读事件，证明是要创建连接
                acceptLink();
            } else if(fd != _listenSocket.GetFd() && FD_ISSET(fd, set)) {
                // 不是监听套接字，但是有读事件，证明可以read
                readData(fd, i);
            } else if(fd != _listenSocket.GetFd() && !FD_ISSET(fd, set)) {
                log(INFO, "%d不是监听套接字, 没有读事件\n", fd);
            } else if(fd == _listenSocket.GetFd() && !FD_ISSET(fd, set)) {
                log(INFO, "%d是监听套接字, 没有读事件\n", fd);
            } else  {
                log(INFO, "其它情况...\n");
            }
        }
        return true;
    }
    
    void printFdArray()
    {
        printf("fdArray: ");
        for(int i = 0; i < ARRAY_SIZE; ++i) {
            if(_fdArray[i] == DEFAULT_FD)
                break;
            printf("%d ", _fdArray[i]);
        }
        printf("\n");
    }
    
    // 创建连接
    void acceptLink()
    {
        std::string ip;
        uint16_t port;
        // 由于此事件已经就绪了，所以accept不会被阻塞，这样就省去了等的过程
        int socket = _listenSocket.Accept(&ip, &port);      
        if(socket < 0)     return;
        log(INFO, "Accept success, ip: %s, port: %d\n", ip.c_str(), port);

        // 该循环会在_fdArray找一个空闲的位置，把连接套接字放进去
        int i = 1;
        for(; i < ARRAY_SIZE; ++i) {
            if(_fdArray[i] != DEFAULT_FD)    
                return;
            else   
                break;
        }
        if(i == ARRAY_SIZE) {
            log(WARNING, "Fd array has no size!\n");
            close(socket);
            return;
        } else {
            _fdArray[i] = socket;
            printFdArray();
        }
    }

    // 读数据
    void readData(int fd, int i)
    {
        char buff[ARRAY_SIZE];
        ssize_t n = read(fd, buff, sizeof buff - 1);
        if(n > 0) {
            buff[n] = 0;
            log(INFO, "echo: %s", buff);
        } else if(n == 0) {
            log(INFO, "read done\n");
            close(fd);
            _fdArray[i] = DEFAULT_FD;       // 将其从数组中移除
        } else {
            log(WARNING, "read error! errno: %d, why: %s\n", errno, strerror(errno));
            close(fd);
            _fdArray[i] = DEFAULT_FD;
        }
    }

    Sock _listenSocket;
    uint16_t _port;
    int* _fdArray;
};

#endif